/*
  Copyright 2002, Blair Kitchen

  This file is part of MySQL-Tcl.

  MySQL-Tcl is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  MySQL-Tcl is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with MySQL-Tcl; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
#ifdef WIN32
#include <windows.h>
#endif

#include <mysql.h>

#include <mysql_tcl.h>

static ConnectionInfo *allocConnectionInfo();
static void freeConnectionInfo(ConnectionInfo *const ptr);

static Tcl_HashTable connectionsTable;

int Mysql_tcl_Init(Tcl_Interp* interp)
{
  //Initialize the stubs library.
  if(Tcl_InitStubs(interp, "8.3", 0)==NULL)
    return(TCL_ERROR);
	
  //Tell Tcl what package we're providing.
  Tcl_PkgProvide(interp, "MySQL-Tcl", "1.0");

  //Initialize the has table which will store all of our connections.
  Tcl_InitHashTable(&connectionsTable, TCL_STRING_KEYS);
	
  //Register our commands under the ::MySQL namespace.
  Tcl_CreateObjCommand(interp, "::MySQL::connect", MySQL_Connect, (ClientData)NULL, NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::disconnect", MySQL_Disconnect, (ClientData)NULL, NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::selectdb", MySQL_SelectDB, (ClientData)NULL, NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::query", MySQL_Query, (ClientData)NULL, NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::nextrow", MySQL_NextRow, (ClientData)NULL,NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::escapestring", MySQL_EscapeString, (ClientData)NULL, NULL);
  Tcl_CreateObjCommand(interp, "::MySQL::numrows", MySQL_NumRows,(ClientData)NULL,NULL);

  //Create an exit handler which will clean up all remaining stuff when Tcl exits.
  Tcl_CreateExitHandler((Tcl_ExitProc*)MySQL_Cleanup,(ClientData)NULL);

  return(TCL_OK);
}

int MySQL_Connect(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  Tcl_HashEntry *hashEnt=NULL;
  char *options[]={"-client_compress","-client_found_rows","-client_ignore_space","-client_interactive","-client_ssl",
		   "-password", "-user", "-connect_timeout","-compressed_protocol","-init_command","-read_default_file",
		   "-read_default_group","-database",NULL};
  MYSQL *conn=NULL;
  ConnectionInfo *connInfo=NULL;
  int connOpts=0;
  char tempString[39];
  char* host=NULL;
  char* user=NULL;
  char* password=NULL;
  char* database=NULL;
  unsigned int port=0;
  int tempInt;	
  int optIndex;
	
  //Check that we have at least three arguments (command, host, and user).
  if(objc<2)
    {
      Tcl_WrongNumArgs(interp, 1, objv, "<host> ?options?");
      return(TCL_ERROR);
    }//end if(objc<3)
	
  //Try to initialize a new connection object.
  if((conn=mysql_init(NULL))==NULL)
    {
      Tcl_SetResult(interp, "Insufficient memory to allocate a new connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if((conn=mysql_init(NULL))==NULL)

  //Get our connection parameters.  We should always have at least a host.
  host=Tcl_GetString(objv[1]);
  //Now parse through the list of options and take the appropriate action.
  int currentOption;
  for(currentOption=2; currentOption<objc; currentOption++)
    {
      if(Tcl_GetIndexFromObj(interp, objv[currentOption], (const char**)options, "option", 0, &optIndex)!=TCL_OK)
	return(TCL_ERROR);
      switch(optIndex)
	{
	case 0://client_compress
	  connOpts |= CLIENT_COMPRESS;
	  break;
	case 1://client_found_rows			
	  connOpts |= CLIENT_FOUND_ROWS;
	  break;
	case 2://client_ignore_space
	  connOpts |= CLIENT_IGNORE_SPACE;
	  break;
	case 3://client_interactive
	  connOpts |= CLIENT_INTERACTIVE;
	  break;
	case 4://client_ssl
	  connOpts |= CLIENT_SSL;
	  break;
	case 5://password
	  password=Tcl_GetString(objv[++currentOption]);
	  break;
	case 6://user
	  user=Tcl_GetString(objv[++currentOption]);
	  break;
	case 7://connect_timeout
	  if(Tcl_GetIntFromObj(interp, objv[++currentOption], &tempInt)!=TCL_OK)
	    return(TCL_ERROR);
	  mysql_options(conn,MYSQL_OPT_CONNECT_TIMEOUT,(char*)&tempInt);
	  break;
	case 8://compressed_protocol
	  mysql_options(conn,MYSQL_OPT_COMPRESS,NULL);
	  break;
	case 9://init_command
	  mysql_options(conn,MYSQL_INIT_COMMAND,Tcl_GetString(objv[++currentOption]));
	  break;
	case 10://read_default_file
	  mysql_options(conn,MYSQL_READ_DEFAULT_FILE,Tcl_GetString(objv[++currentOption]));
	  break;
	case 11://read_default_group
	  mysql_options(conn,MYSQL_READ_DEFAULT_GROUP,Tcl_GetString(objv[++currentOption]));
	  break;
	case 12://database
	  database=Tcl_GetString(objv[++currentOption]);
	  break;
	default:
	  Tcl_SetResult(interp, "Unknown option.", TCL_STATIC);
	  return(TCL_ERROR);
	}//end switch(optIndex)
    }//end for(int currentOption=3; currentOption<objc; currentOption++)

  //Attempt to connect to the server.  Return any error to Tcl.
  if(mysql_real_connect(conn,host,user,password,database,port,NULL,connOpts)==NULL)
    {
      Tcl_SetResult(interp, (char*)mysql_error(conn), TCL_VOLATILE);
      return(TCL_ERROR);
    }//end if(mysql_real_connect(conn,host,user,password,NULL,port,NULL,0)==NULL)

  //Create our connection name based on the connection number appended to 'mysql'.
  sprintf(tempString,"mysql%d",conn);

  //Store our connection information in the hash table.
  connInfo=allocConnectionInfo();
  connInfo->conn=conn;
  connInfo->result=NULL;
  hashEnt=Tcl_CreateHashEntry(&connectionsTable,tempString,&tempInt);
  Tcl_SetHashValue(hashEnt,connInfo);

  //Return our connection name.
  Tcl_SetObjResult(interp, Tcl_NewStringObj(tempString,-1));

  return(TCL_OK);
}

int MySQL_Disconnect(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo *connInfo=NULL;
  Tcl_HashEntry *hashEnt=NULL;

  //Make sure we have the correct number of arguments.
  if(objc!=2)
    {
      Tcl_WrongNumArgs(interp, 1,objv,"<mysql_connection>");
      return(TCL_ERROR);
    }//end if(objc<2 || objc>2 )

  //Find the MySQL connection from its name.
  if((hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1])))==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if((hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1])))==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Delete the hash entry and close the connection.
  Tcl_DeleteHashEntry(hashEnt);
  mysql_free_result(connInfo->result);
  mysql_close(connInfo->conn);
  freeConnectionInfo(connInfo);
	
  return(TCL_OK);
}

int MySQL_Cleanup(ClientData clientData)
{
  ConnectionInfo* connInfo=NULL;
  Tcl_HashEntry* hashEnt;
  Tcl_HashSearch searchToken;

  //Run through the hash table, deleting all hash entries and closing all connections.
  for(hashEnt=Tcl_FirstHashEntry(&connectionsTable,&searchToken); hashEnt!=NULL; hashEnt=Tcl_NextHashEntry(&searchToken))
    {
      connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);
      Tcl_DeleteHashEntry(hashEnt);
      mysql_close(connInfo->conn);
      mysql_free_result(connInfo->result);
      freeConnectionInfo(connInfo);
    }//end for(hashEnt=Tcl_FirstHashEntry(&connectionsTable,&searchToken); hashEnt!=NULL; hashEnt=Tcl_NextHashEntry(hashEnt))
  //Delete the hash table.
  Tcl_DeleteHashTable(&connectionsTable);

  return(TCL_OK);
}

int MySQL_SelectDB(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo* connInfo=NULL;
  Tcl_HashEntry* hashEnt=NULL;

  //Make sure we have the appropriate number of arguments.
  if(objc!=3)
    {
      Tcl_WrongNumArgs(interp, 1,objv,"<mysql_connection> <database_name>");
      return(TCL_ERROR);
    }//end if(objc!=3)

  //Find the MySQL connection in the hash table from the connection name.
  hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1]));
  if(hashEnt==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if(hashEnt==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Ping the server to make sure its still there and reconnect if necessary.
  if(mysql_ping(connInfo->conn))
    {
      Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn), TCL_VOLATILE);
      return(TCL_ERROR);
    }//end if(mysql_ping(conn))

  //Select our database.
  if(mysql_select_db(connInfo->conn,Tcl_GetString(objv[2])))
    {
      Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn), TCL_VOLATILE);
      return(TCL_ERROR);
    }//end if(mysql_select_db(conn,Tcl_GetString(objv[2]))

  return(TCL_OK);
}

int MySQL_Query(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo* connInfo=NULL;
  MYSQL_RES* result=NULL;
  MYSQL_ROW row;
  Tcl_HashEntry* hashEnt=NULL;
  Tcl_Obj* rowList=NULL;
  Tcl_Obj* columnList=NULL;
  char* escapedQuery=NULL;
  unsigned char* query=NULL;
  int queryLen=0;
  unsigned int numFields;
  unsigned long* fieldLens=NULL;
  int useResult=FALSE;

  //Make sure we have the appropriate number of arguments.
  if(objc<3 || objc>4)
    {
      Tcl_WrongNumArgs(interp, 1, objv, "<mysql_connection> <query> ?-use_result?");
      return(TCL_ERROR);
    }//end if(objc!=3)

  if(objc==4)
    {
      if(Tcl_StringCaseMatch(Tcl_GetString(objv[3]),"-use_result",0))
	useResult=TRUE;
    }//end if(objc==4)

  //Find the MySQL connection in the hash table from the connection name.
  hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1]));
  if(hashEnt==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if((hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[2]))==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Check to make sure a query isn't already in progress.
  if(connInfo->result)
    {
      Tcl_SetResult(interp, "Cannot execute query, previous result set present.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if(connInfo->result)

  //Retrieve the query in the form of a byte array (in case there is any binary data.
  query=Tcl_GetByteArrayFromObj(objv[2],&queryLen);
	
  //Ping the server to make sure its still there and reconnect if necessary.
  if(mysql_ping(connInfo->conn))
    {
      Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn), TCL_VOLATILE);
      return(TCL_ERROR);
    }//end if(mysql_ping(conn))

  //Execute the query.
  if(mysql_real_query(connInfo->conn,(const char*)query,queryLen))
    {
      Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn),TCL_VOLATILE);
      return(TCL_ERROR);
    }//end if(mysql_real_query(conn,escapedQuery,escapedQueryLen))

  //If we're going to store the whole result at once, then we put it all into a list to return to Tcl.
  //Otherwise we call mysql_use_result and store the returned value in connInfo->result.
  if(!useResult)
    {
      //Create a list in which to store the incoming rows.
      rowList=Tcl_NewListObj(0,NULL);
	
      //Store the result, if we get a NULL value, make sure it isn't an error.
      if((result=mysql_store_result(connInfo->conn))==NULL)
	{
	  //If field count is not 0, then we should have gotten some sort of result, so we return an error.
	  if(mysql_field_count(connInfo->conn))
	    {
	      Tcl_SetResult(interp, "Couldn't obtain result set.", TCL_STATIC);
	      return(TCL_ERROR);
	    }//end if(numFields)
	  else
	    return(TCL_OK);
	}//end if((result=mysql_store_result())==NULL)

      //For each row, append each field to the return list.
      numFields=mysql_num_fields(result);
      while((row=mysql_fetch_row(result))!=NULL)
	{
	  fieldLens=mysql_fetch_lengths(result);
	  //Fix this section to return each row in a list of columns.
	  Tcl_ListObjAppendElement(interp, rowList, columnList=Tcl_NewListObj(0,NULL));
	  unsigned int i;
	  for(i=0; i<numFields; i++)
	    Tcl_ListObjAppendElement(interp, columnList, Tcl_NewByteArrayObj((unsigned char*)row[i],fieldLens[i]));
	}//end while((row=mysql_fetch_row(result))!=NULL)

      //Free the memory we allocated for the result.
      mysql_free_result(result);

      //Make sure we return the list.
      Tcl_SetObjResult(interp, rowList);
    }//end if(!useResult)
  else
    {
      if((connInfo->result=mysql_use_result(connInfo->conn))==NULL)
	{
	  Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn),TCL_VOLATILE);
	  return(TCL_ERROR);
	}//end if((connInfo->result=mysql_use_result(connInfo->conn))==NULL)
    }//end else

  return(TCL_OK);
}

int MySQL_NextRow(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo* connInfo=NULL;
  Tcl_Obj* toReturn=NULL;
  Tcl_HashEntry* hashEnt=NULL;
  MYSQL_ROW row;
  unsigned int numFields=0;
  unsigned long* fieldLens=NULL;
	
  //Make sure we have the correct number of arguments.
  if(objc!=2)
    {
      Tcl_WrongNumArgs(interp, 1, objv, "<mysql_connection>");
      return(TCL_ERROR);
    }//end if(objc!=2)

  //Get the MySQL connection from the hash table by looking up the connection name.
  if((hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1])))==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if((hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1]))==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Make sure there is a result stored.
  if(!connInfo->result)
    {
      Tcl_SetResult(interp, "connection does not have a result in progress.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if(!connInfo->result)

  //Try to obtain a row from the result.
  if((row=mysql_fetch_row(connInfo->result))!=NULL)
    {
      //Create the Tcl structure and return the result.
      toReturn=Tcl_NewListObj(0,NULL);
      numFields=mysql_num_fields(connInfo->result);
      fieldLens=mysql_fetch_lengths(connInfo->result);
      unsigned int i;
      for(i=0; i<numFields; i++)
	Tcl_ListObjAppendElement(interp, toReturn, Tcl_NewByteArrayObj((unsigned char*)row[i],fieldLens[i]));
      Tcl_SetObjResult(interp, toReturn);
    }//end if(row=mysql_fetch_row(connInfo->result))!=NULL)
  else
    {
      //First we check to see if there was an error.
      if(mysql_errno(connInfo->conn))
	{
	  Tcl_SetResult(interp, (char*)mysql_error(connInfo->conn), TCL_VOLATILE);
	  return(TCL_ERROR);
	}//end if(mysql_errno(connInfo->conn))
      //If there was no error, then we can free the result and return an empty value to indicate we're done.
      else
	{
	  mysql_free_result(connInfo->result);
	  connInfo->result=NULL;
	}//end else
    }//end else

  return(TCL_OK);
}

int MySQL_EscapeString(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo* connInfo=NULL;
  Tcl_HashEntry* hashEnt=NULL;
  unsigned char* buffer=NULL;
  unsigned char* escapedBuffer=NULL;
  int bufferLen=0;
  int escapedBufferLen=0;

  //Make sure we have the appropriate number of arguments.
  if(objc!=3)
    {
      Tcl_WrongNumArgs(interp, 1, objv, "<mysql_connection> <string>");
      return(TCL_ERROR);
    }//end if(objc!=3)
	
  //Find the MySQL connection in the hash table from the connection name.
  if((hashEnt=Tcl_FindHashEntry(&connectionsTable, Tcl_GetString(objv[1])))==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if((hashEnt=Tcl_FindHashEntry(&connectionsTable, Tcl_GetString(objv[1])))==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Get the string to escape in the form of a byte array (it could have binary data).
  buffer=Tcl_GetByteArrayFromObj(objv[2], &bufferLen);
  //Allocate space for the escaped string (the length of the string to escape * 2 + 1 for the terminating NULL).
  escapedBuffer=ckalloc((bufferLen*2)+1);

  //Escape the string.
  escapedBufferLen=mysql_real_escape_string(connInfo->conn,(char*)escapedBuffer,(const char*)buffer,bufferLen);
	
  //Return the escaped string in the form of a byte array.
  Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char*)escapedBuffer,escapedBufferLen));

  //Delete the escaped buffer, it's no longer needed as Tcl has made a copy of it.
  ckfree(escapedBuffer);

  return(TCL_OK);
}

int MySQL_NumRows(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])
{
  ConnectionInfo* connInfo=NULL;
  Tcl_HashEntry* hashEnt=NULL;

  //Make sure we have the appropriate number of arguments.
  if(objc!=2)
    {
      Tcl_WrongNumArgs(interp, 1,objv,"<mysql_connection>");
      return(TCL_ERROR);
    }//end if(objc!=2)

  //Find the MySQL connection in the hash table from the connection name.
  hashEnt=Tcl_FindHashEntry(&connectionsTable,Tcl_GetString(objv[1]));
  if(hashEnt==NULL)
    {
      Tcl_SetResult(interp, "Invalid mysql_connection.", TCL_STATIC);
      return(TCL_ERROR);
    }//end if(hashEnt==NULL)

  connInfo=(ConnectionInfo*)Tcl_GetHashValue(hashEnt);

  //Obtain the number of affected rows and return it to Tcl.
  Tcl_SetObjResult(interp, Tcl_NewLongObj((long)mysql_affected_rows(connInfo->conn)));

  return(TCL_OK);
}

static ConnectionInfo *allocConnectionInfo()
{
  return((ConnectionInfo*)ckalloc(sizeof(ConnectionInfo)));
}

static void freeConnectionInfo(ConnectionInfo *const ptr)
{
  ckfree((void*)ptr);
}

